/* Archive Extractor
 * Copyright 2008-2009 Matthew Hoops
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * $URL: http://archive-extractor.googlecode.com/svn/trunk/main.cpp $
 * $Id: main.cpp 5 2009-03-24 02:26:27Z clone2727 $
 *
 */

// C-Standard Libraries
#include <cstdio>

// Base files of this app
#include "arc_util.h"
#include "arc_strings.h"
#include "file_reader.h"
#include "archive.h"

// Individual archive includes
#include "binhex.h"
#ifdef ENABLE_BZIP2
#	include "bzip2.h"
#endif
#include "d64.h"
#include "ea_wad.h"
#ifdef ENABLE_ZLIB
#	include "gzip.h"
#endif
#include "gob.h"
#include "iso_image.h"
#include "lab.h"
#include "ms_cab.h"
#include "ms_comp.h"
#include "quakepak.h"
#include "rar.h"
#include "stuffit.h"
#include "tar.h"
#include "zip.h"

// The individual values of the version
#define ARC_EX_VERSION_MAJOR  "0"
#define ARC_EX_VERSION_MINOR  "1"
#define ARC_EX_VERSION_BUGFIX "4svn"

// This puts the versions together. This should not be modified.
#define ARC_EX_VERSION ARC_EX_VERSION_MAJOR"."ARC_EX_VERSION_MINOR"."ARC_EX_VERSION_BUGFIX

enum {
	kUnknownFileType = -1
};

const char* getLastExtension(const char *filename) {
	// First, let's check for the extension
	char *extension = strstr(filename, ".");
	if (!extension)
		return NULL;
	for (char *temp = strstr(extension + 1, "."); temp; temp = strstr(extension + 1, "."))
		extension = temp;		
	return extension + 1;
}

bool compareExtensionString(const char* extension, const char* target) {
	// First test against no extension
	if (!extension && target[0] == 0)
		return true;

	uint32 extensionLength = strlen(extension);
	
	// There's no point in checking if the lengths are different
	if (extensionLength != strlen(target))
		return false;
		
	// Do a character by character check, ignore wildcard characters
	for (uint32 i = 0; i < extensionLength; i++)
		if (target[i] != '?' && extension[i] != target[i])
			return false;
	
	return true;
}

std::vector<int> detectArchiveTypeExtension(const char* extension, std::vector<Archive*> archiveList) {
	std::vector<int> detectedList;
	for (uint32 i = 0; i < archiveList.size(); i++) {
		std::vector<const char*> extensionList = archiveList.at(i)->getArchiveExtensions();
		for (uint32 j = 0; j < extensionList.size(); j++)
			if (compareExtensionString(extension, extensionList.at(j))) {
				detectedList.push_back(i);
				break;
			}
	}
	return detectedList;
}

int detectArchiveTypeBinary(const char* filename, std::vector<Archive*> archiveList) {
	// Otherwise, let's test the rest
	int bestMatch = 0;
	int32 bestMatchIndex = kUnknownFileType;
	for (uint32 i = 0; i < archiveList.size(); i++) {
		int testMatch = archiveList.at(i)->detect(filename);
		if (testMatch == 100) { // We already found something with 100% certainty, there's no way it's anything else...
			return i;
		} else if (testMatch > bestMatch) {
			bestMatch = testMatch;
			bestMatchIndex = i;
		}
	}
	
	return bestMatchIndex;
}

void printUsage(const char* appName, std::vector<Archive*> archiveList) {
	printf ("Usage: %s [flags] <archive>\n"
			"    Archive Extractor v"ARC_EX_VERSION"\n"
			"    Supported Archives:\n"
			, appName);
	for (uint32 i = 0; i < archiveList.size(); i++)
		printf ("      - %s\n", archiveList.at(i)->getArchiveName());
	printf ("\n");
	printf ("    Flags:\n"
			"        Long Flag              Short Flag       Description\n"
			"        =======================================================================\n"
			"        --help                 -h               Display Help and Quit\n"
			"        --version                               Display Version Information and\n"
			"                                                Quit\n"
			"        --output=FILE                           Set the Output File Name for\n"
			"                                                Certain Archive Types (ie.\n"
			"                                                Microsoft Compressed)\n"
			"        --file=FILE                             Set Input File Name\n"
			"        --verbose                               Make the Extraction Process\n"
			"                                                More Verbose\n"
			);
}

enum {
	kCmdQuit = (1 << 0),
	kCmdVerbose = (1 << 1),
	kCmdListFilesOnly = (1 << 2)
};

struct CommandLineKey {
	bool hasFile;
	const char* filename;
	uint32 flags;
	bool hasOutputFile;
	const char* outputName;
};

const char* getLargeFlagArg(const char* largeFlag) {
	char* equalsString = strstr(largeFlag, "=");
	if (!equalsString)
		return NULL;
	equalsString++;
	if (strstr(equalsString, "="))
		return NULL;
	return equalsString;
}

#if 0
const char* getSmallFlagArg(const char* smallFlag) {
	if (smallFlag[2] == 0)
		return NULL;
	return smallFlag + 2;
}
#endif

CommandLineKey parseCommandLine(int argc, const char **argv, std::vector<Archive*> archiveList) {
	CommandLineKey key;
	key.hasFile = false;
	key.hasOutputFile = false;
	key.flags = 0;
	
	if (argc == 1) {
		printUsage(argv[0], archiveList);
		key.flags |= kCmdQuit;
		return key;
	}
	
	for (int i = 1; i < argc; i++) {
		bool largeFlag = (argv[i][0] == '-') && (argv[i][1] == '-');
		bool smallFlag = !largeFlag && (argv[i][0] == '-');
		if (smallFlag) {
			//const char* arg = getSmallFlagArg(argv[i]);
			if (!stricmp(argv[i] + 1, "h")) {
				printUsage(argv[0], archiveList);
				key.flags |= kCmdQuit;
				break;
			} else {
				printf ("Unknown flag \'-%c\'\n", argv[i][1]);
				key.flags |= kCmdQuit;
				break;
			}
		} else if (largeFlag) {
			const char* arg = getLargeFlagArg(argv[i]);
			if (!stricmp(argv[i] + 2, "help")) {
				printUsage(argv[0], archiveList);
				key.flags |= kCmdQuit;
				break;
			} else if (!stricmp(argv[i] + 2, "version")) {
				printf ("Archive Extractor Version: "ARC_EX_VERSION"\n");
				key.flags |= kCmdQuit;
				break;
			} else if (!stricmp(argv[i] + 2, "verbose")) {
				key.flags |= kCmdVerbose;
			} else if (!strnicmp(argv[i] + 2, "output", 6)) {
				if (key.hasOutputFile) {
					printf ("An output file has already been specified!\n");
					key.flags |= kCmdQuit;
					break;
				} else if (!arg) {
					printf ("No output file has already been specified when using --output!\n");
					key.flags |= kCmdQuit;
					break;
				} else {
					key.hasOutputFile = true;
					key.outputName = arg;
				}
			} else if (!strnicmp(argv[i] + 2, "file", 4)) {
				if (key.hasFile) {
					printf ("An input file has already been specified!\n");
					key.flags |= kCmdQuit;
					break;
				} else if (!arg) {
					printf ("No input file has already been specified when using --file!\n");
					key.flags |= kCmdQuit;
					break;
				} else {
					key.hasFile = true;
					key.filename = arg;
				}
			} else {
				printf ("Unknown flag \'%s\'\n", argv[i]);
				key.flags |= kCmdQuit;
				break;
			}
		} else if (i == (argc - 1) && !key.hasFile) {
			key.filename = argv[i];
			key.hasFile = true;
		} else {
			printf ("Unknown token \'%s\'\n", argv[i]);
			key.flags |= kCmdQuit;
			break;
		}
	}
	return key;
}

#define ADD_ARCHIVE(x) archiveList.push_back(new x())

int main(int argc, const char **argv) {	
	// Declare New Archives Here!
	std::vector<Archive*> archiveList;
	
	// This order is to retain the order in which they were added. Elsewhere, they are listed alphabetically.
	ADD_ARCHIVE(ZipArchive);
	ADD_ARCHIVE(D64Image);
	ADD_ARCHIVE(UnixTapeArchive);
#ifdef ENABLE_ZLIB
	ADD_ARCHIVE(GZipArchive);
#endif
#ifdef ENABLE_BZIP2
	ADD_ARCHIVE(BZip2Archive);
#endif
	ADD_ARCHIVE(RarArchive);
	ADD_ARCHIVE(MicrosoftCabinet);
	ADD_ARCHIVE(MicrosoftCompressedArchive);
	ADD_ARCHIVE(IsoDiscImage);
	ADD_ARCHIVE(GobArchive);
	ADD_ARCHIVE(LabArchive);
	ADD_ARCHIVE(BinHexArchive);
	ADD_ARCHIVE(StuffItArchive);
	ADD_ARCHIVE(QuakePakArchive);
	ADD_ARCHIVE(EAWADArchive);
	
	CommandLineKey key = parseCommandLine(argc, argv, archiveList);
	
	if (key.flags & kCmdQuit)
		return 0;
	
	if (!key.hasFile) {
		printf ("No filename specified!\n");
		return 0;
	}
	
	// Let's make sure the file exists
	FILE *inputFile = fopen(key.filename, "rb");
	if (!inputFile) {
		printf ("Could not open file \'%s\'\n", key.filename);
		return 0;
	}
	fclose(inputFile);
	
	// Let's detect by examining the file.
	int binaryDetection = detectArchiveTypeBinary(key.filename, archiveList);
	
	if (binaryDetection >= 0) { // We have a match! :D
		if (key.flags & kCmdVerbose)
			printf ("Detected \'%s\' as type \'%s\'.\nAttempting to extract data...\n", key.filename, archiveList.at(binaryDetection)->getArchiveName());
	
		// Attempt extraction
		if (archiveList.at(binaryDetection)->extractFiles(key.filename))
			printf("Successful extraction from \'%s\'\n", key.filename);
		else
			printf("Failed extraction from \'%s\'\n", key.filename);
	} else { // Not all files have a signature that we can match, however...
		if (key.flags & kCmdVerbose)
			printf ("Could not match the format to any known format using binary detection!\n"
					"Attempting to match via extension...\n");
					
		std::vector<int> extensionDetection = detectArchiveTypeExtension(getLastExtension(key.filename), archiveList);
		if (extensionDetection.size() == 1) {
			// TODO!
			printf ("This file has a known extension, but an unknown/unhandled format!\n");
		} else if (extensionDetection.size() > 0) {
			// TODO! (This currently is not possible under the current code).
		} else {
			// Failed all detection
			printf ("This file is completely unknown/unhandled!\n");
		}
	}

	archiveList.clear();
	
	return 0;
}
